﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using SharpRomans.Support;
using SharpRomans.ParsingRules;

namespace SharpRomans
{
	public class RomanNumeral : IComparable<RomanNumeral>, IComparable<short>, IComparable<RomanFigure>, IEquatable<RomanNumeral>, IEquatable<short>, IEquatable<RomanFigure>, IConvertible, IValuable
	{
		public static readonly short MinValue = default(short);
		public static readonly short MaxValue = 3999;
		private static readonly Range<int> _validity = new Range<int>(MinValue, MaxValue);

		public short Value { get; private set; }
		public IEnumerable<RomanFigure> Figures { get; private set; }
		private readonly string _figures;

		private static readonly Tuple<RomanFigure[], short>[] _calculationTable = new[]
		{
			RomanFigure.M.AsCalculationRow(),
			calculationRow(900, RomanFigure.C, RomanFigure.M),
			RomanFigure.D.AsCalculationRow(),
			calculationRow(400, RomanFigure.C, RomanFigure.D),
			RomanFigure.C.AsCalculationRow(),
			calculationRow(90, RomanFigure.X, RomanFigure.C),
			RomanFigure.L.AsCalculationRow(),
			calculationRow(40, RomanFigure.X, RomanFigure.L),
			RomanFigure.X.AsCalculationRow(),
			calculationRow(9, RomanFigure.I, RomanFigure.X),
			RomanFigure.V.AsCalculationRow(),
			calculationRow(4, RomanFigure.I, RomanFigure.V),
			RomanFigure.I.AsCalculationRow()
		};

		private static Tuple<RomanFigure[], short> calculationRow(short value, params RomanFigure[] figures)
		{
			return new Tuple<RomanFigure[], short>(figures, value);
		}

		public RomanNumeral(short number)
		{
			AssertRange(number);

			Value = number;

			var sb = new StringBuilder();
			if (number.Equals(0))
			{
				Figures = new[] { RomanFigure.N };
				sb.Append(RomanFigure.N.ToString(CultureInfo.InvariantCulture));
			}
			else
			{
				var figures = new List<RomanFigure>();
				for (int i = 0; i < _calculationTable.Length; i++)
				{
					while (number >= _calculationTable[i].Item2)
					{
						number -= _calculationTable[i].Item2;
						figures.AddRange(_calculationTable[i].Item1);
						sb.Append(string.Concat(_calculationTable[i].Item1.Select(f => f.ToString(CultureInfo.InvariantCulture)).ToArray()));
					}
				}
				Figures = figures;
			}

			_figures = sb.ToString();
		}

		public static bool CheckRange(short value)
		{
			return _validity.Contains(value);
		}

		public static void AssertRange(short value)
		{
			if (!CheckRange(value)) throw new Exceptions.NumeralOutOfRangeException("value", value);
		}

		#region comparisons

		public int CompareTo(RomanNumeral other)
		{
			return other == (RomanNumeral)null ? 1 : Value.CompareTo(other.Value);
		}

		#region operators

		public static bool operator >(RomanNumeral left, RomanNumeral right)
		{
			return left != (RomanNumeral)null && left.CompareTo(right) > 0;
		}

		public static bool operator >=(RomanNumeral left, RomanNumeral right)
		{
			return left == (RomanNumeral)null ? right == (RomanNumeral)null : left.CompareTo(right) >= 0;
		}

		public static bool operator <(RomanNumeral left, RomanNumeral right)
		{
			return left == (RomanNumeral)null ? right != (RomanNumeral)null : left.CompareTo(right) < 0;
		}

		public static bool operator <=(RomanNumeral left, RomanNumeral right)
		{
			return left == (RomanNumeral)null || left.CompareTo(right) <= 0;
		}

		#endregion

		public int CompareTo(short other)
		{
			return Value.CompareTo(other);
		}

		#region operators

		public static bool operator >(RomanNumeral left, short right)
		{
			return left != (RomanNumeral)null && left.CompareTo(right) > 0;
		}

		public static bool operator >=(RomanNumeral left, short right)
		{
			return left != (RomanNumeral)null && left.CompareTo(right) >= 0;
		}

		public static bool operator <(RomanNumeral left, short right)
		{
			return left == (RomanNumeral)null || left.CompareTo(right) < 0;
		}

		public static bool operator <=(RomanNumeral left, short right)
		{
			return left == (RomanNumeral)null || left.CompareTo(right) <= 0;
		}

		public static bool operator >(short left, RomanNumeral right)
		{
			return right == (RomanNumeral)null || right.CompareTo(left) < 0;
		}

		public static bool operator >=(short left, RomanNumeral right)
		{
			return right == (RomanNumeral)null || right.CompareTo(left) <= 0;
		}

		public static bool operator <(short left, RomanNumeral right)
		{
			return right != (RomanNumeral)null && right.CompareTo(left) > 0;
		}

		public static bool operator <=(short left, RomanNumeral right)
		{
			return right != (RomanNumeral)null && right.CompareTo(left) >= 0;
		}

		#endregion

		public int CompareTo(RomanFigure other)
		{
			return other == null ? 1 : Value.CompareTo(other.Value);
		}

		#region operators

		public static bool operator >(RomanNumeral left, RomanFigure right)
		{
			return left != (RomanNumeral)null && left.CompareTo(right) > 0;
		}

		public static bool operator >=(RomanNumeral left, RomanFigure right)
		{
			return left == (RomanNumeral)null ? right == null : left.CompareTo(right) >= 0;
		}

		public static bool operator <(RomanNumeral left, RomanFigure right)
		{
			return left == (RomanNumeral)null ? right != null : left.CompareTo(right) < 0;
		}

		public static bool operator <=(RomanNumeral left, RomanFigure right)
		{
			return left == (RomanNumeral)null || left.CompareTo(right) <= 0;
		}

		#endregion

		#endregion

		#region equality

		public override bool Equals(object obj)
		{
			if (ReferenceEquals(null, obj)) return false;
			if (ReferenceEquals(this, obj)) return true;

			if (obj is short) return Equals((short)obj);
			if (obj is RomanNumeral) return Equals((RomanNumeral)obj);
			if (obj is RomanFigure) return Equals((RomanFigure)obj);
			return false;
		}

		public override int GetHashCode()
		{
			return Value.GetHashCode();
		}

		public bool Equals(RomanNumeral numeral)
		{
			if (ReferenceEquals(null, numeral)) return false;
			if (ReferenceEquals(this, numeral)) return true;
			return numeral.Value == Value;
		}

		public bool Equals(short figure)
		{
			return figure == Value;
		}

		public bool Equals(RomanFigure figure)
		{
			if (ReferenceEquals(null, figure)) return false;
			return figure.Value == Value;
		}

		#region operators

		public static bool operator ==(RomanNumeral left, RomanNumeral right)
		{
			return EqualityComparer<RomanNumeral>.Default.Equals(left, right);
		}

		public static bool operator !=(RomanNumeral left, RomanNumeral right)
		{
			return !EqualityComparer<RomanNumeral>.Default.Equals(left, right);
		}

		public static bool operator ==(RomanNumeral left, short right)
		{
			if (left == (RomanNumeral)null) return false;
			return left.Equals(right);
		}

		public static bool operator !=(RomanNumeral left, short right)
		{
			if (left == (RomanNumeral)null) return true;
			return !left.Equals(right);
		}

		public static bool operator ==(short left, RomanNumeral right)
		{
			if (right == (RomanNumeral)null) return false;
			return right.Equals(left);
		}

		public static bool operator !=(short left, RomanNumeral right)
		{
			if (right == (RomanNumeral)null) return true;
			return !right.Equals(left);
		}

		public static bool operator ==(RomanNumeral left, RomanFigure right)
		{
			return left == (RomanNumeral) null ? right == null : left.Equals(right);
		}

		public static bool operator !=(RomanNumeral left, RomanFigure right)
		{
			return left == (RomanNumeral) null ? right != null : !left.Equals(right);
		}

		#endregion

		#endregion

		public override string ToString()
		{
			return _figures;
		}

		public static readonly RomanNumeral Min = new RomanNumeral(MinValue);
		public static readonly RomanNumeral Max = new RomanNumeral(MaxValue);
		public static readonly RomanNumeral Zero = new RomanNumeral(0);

		#region conversions

		public static explicit operator short(RomanNumeral numeral)
		{
			if (numeral == (RomanNumeral)null) throw new ArgumentNullException("numeral");

			return numeral.Value;
		}

		public static explicit operator RomanNumeral(short number)
		{
			return new RomanNumeral(number);
		}

		#region IConvertible

		TypeCode IConvertible.GetTypeCode()
		{
			return TypeCode.Object;
		}

		bool IConvertible.ToBoolean(IFormatProvider provider)
		{
			return Convert.ToBoolean(Value);
		}

		char IConvertible.ToChar(IFormatProvider provider)
		{
			return Convert.ToChar(_figures);
		}

		sbyte IConvertible.ToSByte(IFormatProvider provider)
		{
			return Convert.ToSByte(Value);
		}

		byte IConvertible.ToByte(IFormatProvider provider)
		{
			return Convert.ToByte(Value);
		}

		short IConvertible.ToInt16(IFormatProvider provider)
		{
			return Value;
		}

		ushort IConvertible.ToUInt16(IFormatProvider provider)
		{
			return Convert.ToUInt16(Value);
		}

		int IConvertible.ToInt32(IFormatProvider provider)
		{
			return Value;
		}

		uint IConvertible.ToUInt32(IFormatProvider provider)
		{
			return Convert.ToUInt32(Value);
		}

		long IConvertible.ToInt64(IFormatProvider provider)
		{
			return Value;
		}

		ulong IConvertible.ToUInt64(IFormatProvider provider)
		{
			return Convert.ToUInt64(Value);
		}

		float IConvertible.ToSingle(IFormatProvider provider)
		{
			return Value;
		}

		double IConvertible.ToDouble(IFormatProvider provider)
		{
			return Value;
		}

		decimal IConvertible.ToDecimal(IFormatProvider provider)
		{
			return Value;
		}

		DateTime IConvertible.ToDateTime(IFormatProvider provider)
		{
			throw invalidCast(typeof(DateTime));
		}

		private InvalidCastException invalidCast(Type other)
		{
			return new InvalidCastException(string.Format("Invalid cast from '{0}' to '{1}'", typeof(RomanFigure).Name, other.Name));
		}

		public string ToString(IFormatProvider provider)
		{
			return _figures.ToString(provider);
		}

		object IConvertible.ToType(Type conversionType, IFormatProvider provider)
		{
			// only called when another CLR type is passed. So unless it is itself (weird) it cannot be converted
			if (conversionType == typeof(RomanNumeral)) return this;
			throw invalidCast(conversionType);
		}

		#endregion

		#endregion

		#region arithmetic

		public RomanNumeral Plus(RomanNumeral numeral)
		{
			return withValue(numeral, (value, valuable) => (short)(value + valuable.Value));
		}

		public RomanNumeral Plus(RomanFigure figure)
		{
			return withValue(figure, (value, valuable) => (short)(value + valuable.Value));
		}

		public static RomanNumeral operator +(RomanNumeral left, RomanNumeral right)
		{
			return left == (RomanNumeral)null ? right : left.Plus(right);
		}

		public static RomanNumeral operator +(RomanNumeral left, RomanFigure right)
		{
			return left == (RomanNumeral)null ?
				(right == null ? null : new RomanNumeral(right.Value)) :
				left.Plus(right);
		}

		public RomanNumeral Minus(RomanNumeral numeral)
		{
			return withValue(numeral, (value, valuable) => (short)(value - valuable.Value));
		}

		public RomanNumeral Minus(RomanFigure figure)
		{
			return withValue(figure, (value, valuable) => (short)(value - valuable.Value));
		}

		public static RomanNumeral operator -(RomanNumeral left, RomanNumeral right)
		{
			return left == (RomanNumeral)null ? right : left.Minus(right);
		}

		public static RomanNumeral operator -(RomanNumeral left, RomanFigure right)
		{
			return left == (RomanNumeral)null ?
				(right == null ? null : new RomanNumeral(right.Value)) :
				left.Minus(right);
		}

		private RomanNumeral withValue(IValuable valuable, Func<short, IValuable, short> op)
		{
			if (valuable == null) return this;

			var result = op(Value, valuable);

			AssertRange(result);
			return new RomanNumeral(result);
		}

		#endregion

		public static RomanNumeral Parse(string literal)
		{
			IConnectableObservable<char> observable = literal.ToObservable().Publish();
			var figurable = new FigurableCharacter(literal);
			var figurableObserver = observable.Subscribe(figurable);

			var zero = new Zero(literal);
			var zeroObserver = observable.Subscribe(zero);
			try
			{
				observable.Connect();
			}
			finally
			{
				figurableObserver.Dispose();
				zeroObserver.Dispose();
			}

			return null;
		}
	}
}
